from torch import nn, cat
from torchvision import models

class ResNet(nn.Module):
    def __init__(self, block, out_channels, num_block, classes):
        super().__init__()
        self.in_channel = 64
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, 7, 2, 3, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(True),
            nn.MaxPool2d(3, 2, 1)
        )
        self.conv2 = self.__make_block(block, out_channels[0], num_block[0], 1)
        self.conv3 = self.__make_block(block, out_channels[1], num_block[1], 2)
        self.conv4 = self.__make_block(block, out_channels[2], num_block[2], 2)
        self.conv5 = self.__make_block(block, out_channels[3], num_block[3], 2)
        self.pool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(out_channels[3] * block.expansion, classes)
        self.__init_args()

    def __make_block(self, block, out_channel, block_num, stride):
        layers = []
        strides = [stride]
        # --------------------------------------------------------- #
        # 通过block_num和stride组合控制残差块是否需要下采样
        # --------------------------------------------------------- #
        strides.extend([1]*(block_num - 1))
        for stride in strides:
            layers.append(
                block(self.in_channel, out_channel, stride)
            )
            self.in_channel = out_channel * block.expansion
        return nn.Sequential(*layers)

    def __init_args(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, std=1e-3)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)

    def forward(self, x):
        out = self.conv1(x)
        out = self.conv2(out)
        out = self.conv3(out)
        out = self.conv4(out)
        out = self.conv5(out)
        out = self.pool(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out


class Residual(nn.Module):

    expansion = 1

    def __init__(self, in_channel, out_channel, stride=1):
        super().__init__()
        self.in_channel, self.out_channel, self.stride = in_channel, out_channel, stride
        # init model
        self.__init_module()
        self.relu = nn.ReLU(True)

    def __init_module(self):
        self.block = nn.Sequential(
            # 假设为conv3,4,5那么就需要第一步进行下采样
            nn.Conv2d(self.in_channel, self.out_channel, 3, padding=1, stride=self.stride, bias=False),
            nn.BatchNorm2d(self.out_channel),
            nn.ReLU(True),
            nn.Conv2d(self.out_channel, self.out_channel, 3, padding=1, bias=False),
            nn.BatchNorm2d(self.out_channel)
        )

        self.shotcut = nn.Sequential()
        # --------------------------------------------------------- #
        # in_channel与out_channel判断是否输出通道和输入通道一致
        # 也就是用于从上一个残差块传过来的数据
        # stride的目的就是为了使前向传播的channel相匹配
        # --------------------------------------------------------- #
        if self.in_channel != self.out_channel:
            self.shotcut = nn.Sequential(
                nn.Conv2d(self.in_channel, self.out_channel, 1, stride=self.stride, bias=False),
                nn.BatchNorm2d(self.out_channel)
            )

    def forward(self, x):
        identify = self.shotcut(x)
        out = self.block(x)
        out = out + identify
        out = self.relu(out)
        return out

class BottolBlock(nn.Module):

    expansion = 4

    def __init__(self, in_channel, out_channel, stride):
        super().__init__()
        self.in_channel, self.out_channel, self.stride = in_channel, out_channel, stride
        self.__create_block()

    def __create_block(self):
        # ------------------------------------------------------- #
        # in_channel = 64, out_channel = 64
        # conv1_block1 ==> 64->64, 64->64, 64->256  shotcut ==> 64->256
        # conv1_block2 ==> 256->64, 64->64, 64->256 shotcut ==> 256->256
        # conv1_block3 ==> 256->64, 64->64, 64->256 shotcut ==> 256->256
        # ------------------------------------------------------- #
        self.block = nn.Sequential(
            nn.Conv2d(self.in_channel, self.out_channel, 1, bias=False),
            nn.BatchNorm2d(self.out_channel),
            nn.ReLU(True),
            nn.Conv2d(self.out_channel, self.out_channel, 3, stride=self.stride, padding=1, bias=False),
            nn.BatchNorm2d(self.out_channel),
            nn.ReLU(True),
            nn.Conv2d(self.out_channel, self.out_channel * self.expansion, 1, bias=False),
            nn.BatchNorm2d(self.out_channel * self.expansion)
        )

        self.shotcut = nn.Sequential()
        if self.in_channel != self.out_channel * self.expansion:
            self.shotcut = nn.Sequential(
                nn.Conv2d(self.in_channel, self.out_channel * self.expansion, 1, stride=self.stride, bias=False),
                nn.BatchNorm2d(self.out_channel * self.expansion)
            )

        self.relu = nn.ReLU(True)

    def forward(self, x):
        out = self.block(x) + self.shotcut(x)
        out = self.relu(out)
        return out


def resnet18(classes):
    return ResNet(Residual, [64, 128, 256, 512], [2, 2, 2, 2], classes)

def resnet34(classes):
    return ResNet(Residual, [64, 128, 256, 512], [3, 4, 6, 3], classes)

def resnet50(classes):
    return ResNet(BottolBlock, [64, 128, 256, 512], [3, 4, 6, 3], classes)

def resnet101(classes):
    return ResNet(BottolBlock, [64, 128, 256, 512], [3, 4, 23, 3], classes)

def resnet152(classes):
    return ResNet(BottolBlock, [64, 128, 256, 512], [3, 8, 36, 3], classes)

if __name__ == "__main__":
    from utils.utils import get_model_flops_args
    get_model_flops_args(ResNet(Residual, [64, 128, 256, 512], [3, 4, 6, 3], 5), (3, 640, 640))